// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using SnapObjects.Commons;

namespace SnapObjects.Expressions
{
    public class CompileScript : ICompileScript
    {
        public CompileScript(Expression body, ParserOptions<Expression> options)
        {
            this.Lambda = options is CompileParserOptions context
                ? this.IntegrationParameter(body, context)
                : Expression.Lambda(body);
        }

        public LambdaExpression Lambda { get; }

        public LambdaDelegate Compile()
        {
            return this.Compile(this.Lambda);
        }

        private LambdaDelegate Compile(LambdaExpression lambda)
        {
            var fn = lambda.Compile();

            return new LambdaDelegate(args =>
            {
                if (lambda.Parameters.Count <= 0)
                {
                    return fn.DynamicInvoke();
                }

                var parameters = this.MatchParameters(lambda, args);

                return fn.DynamicInvoke(parameters.ToArray());
            });
        }

        private IEnumerable<object> MatchParameters(LambdaExpression lambda, IEnumerable<object> args)
        {
            var parames = args.ToList();
            var parameters = new List<object>();

            foreach (var parameter in lambda.Parameters)
            {
                var arg = parames.FirstOrDefault(
                    m => m != null && parameter.Type.IsAssignableFrom(m.GetType()));

                if (arg == null)
                {
                    var argument = parames.FirstOrDefault(
                        m => m != null && m is IDictionary<string, object> dic);
                    
                    if (argument != null && 
                        ((IDictionary<string, object>)argument).TryGetValue(parameter.Name, out var value))
                    {
                        value = Utils.ChangeType(value, parameter.Type);

                        parameters.Add(value);
                    }
                    else
                    {
                        var index = parames.IndexOf(null);

                        if (index > -1 && parameter.Type.CanAssignedNull())
                        {
                            arg = parames[index];

                            parameters.Add(arg);
                            parames.Remove(arg);
                        }
                        else
                        {
                            throw new TargetParameterCountException(
                                $"{parameter.Name} has declared, but did not pass parameters to it");
                        }
                    } 
                }
                else
                {
                    parameters.Add(arg);
                    parames.Remove(arg);
                }
            }

            return parameters;
        }

        private LambdaExpression IntegrationParameter(Expression body, CompileParserOptions options)
        {
            var useds = options.UsedParameters.ToList();

            var lambda = Expression.Lambda(body, useds);

            var lookup = useds.ToLookup(x => x.Type);

            var newUseds = new List<ParameterExpression>();

            foreach (var group in lookup)
            {
                if (group.Key != typeof(object) &&
                    (group.Key.IsClass || group.Key.IsInterface))
                {
                    ParameterExpression paramter = null;

                    foreach (var item in group)
                    {
                        if (paramter == null)
                        {
                            paramter = item;

                            newUseds.Add(item);
                        }

                        var index = useds.IndexOf(item);

                        useds[index] = paramter;
                    }
                }
                else
                {
                    newUseds.AddRange(group);
                }
            }

            if (useds.Count != newUseds.Count)
            {
                var invoke = Expression.Invoke(lambda, useds);

                lambda = Expression.Lambda(invoke, newUseds);
            }

            return lambda;
        }
    }
}
